package com.yvan.leto.intrenal;

import com.google.common.collect.Maps;
import com.yvan.leto.LetoManage;
import com.yvan.leto.ReloadableConfigurationProperties;
import com.yvan.leto.ReloadableProperty;
import com.yvan.leto.boot.LetoSpringContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;


/**
 * Created by xiaoEight on 2018/6/14.
 */
@Component
@Slf4j
public class ReloadablePropertyPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {

    private Map<String, Set<BeanPropertyHolder>> beanConfigurationPropertiesSubscriptions = Maps.newHashMap();

    private Map<String, Set<BeanPropertyHolder>> beanPropertySubscriptions = Maps.newHashMap();

    @PostConstruct
    protected void init() {
        log.info("Registering ReloadablePropertyProcessor for properties changes");
        LetoSpringContext.setReloadablePropertyPostProcessor(this);
    }

    @Override
    public boolean postProcessAfterInstantiation(final Object bean, final String beanName) throws BeansException {
        if (log.isDebugEnabled()) {
            log.debug("Setting Reloadable Properties on [{}]", beanName);
        }
        setPropertiesOnBean(bean);
        return true;
    }

    public void updatePropertiesOnBean() {
        for (Set<BeanPropertyHolder> beanPropertyHolders:beanPropertySubscriptions.values()){
            for(BeanPropertyHolder beanPropertyHolder:beanPropertyHolders){
                setBeanFields(beanPropertyHolder.getBean());
            }
        }

        for (Set<BeanPropertyHolder> beanPropertyHolders:beanConfigurationPropertiesSubscriptions.values()){
            for(BeanPropertyHolder beanPropertyHolder:beanPropertyHolders){
                setConfigurationPropertiesBean(beanPropertyHolder.getBean());
            }
        }
    }

    private void setPropertiesOnBean(final Object bean) {

        setBeanFields(bean);

        setConfigurationPropertiesBean(bean);
    }

    private void setBeanFields(final Object bean){
        ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() {

            @Override
            public void doWith(final Field field) throws IllegalArgumentException, IllegalAccessException {

                final ReloadableProperty annotation = field.getAnnotation(ReloadableProperty.class);
                if (null != annotation) {

                    ReflectionUtils.makeAccessible(field);
                    validateFieldNotFinal(bean, field);

                    final Object property =  getProperty(annotation.value(),field.getType());
                    validatePropertyAvailableOrDefaultSet(bean, field, annotation, property);

                    if (null != property) {

                        log.info("Attempting to convert and set property [{}] on field [{}] for class [{}] to type [{}]",
                                new Object[]{property, field.getName(), bean.getClass()
                                        .getCanonicalName(), field.getType()});

                        log.info("Setting field [{}] of class [{}] with value [{}]", new Object[]{field.getName(), bean.getClass()
                                .getCanonicalName(), property});

                        field.set(bean, property);

                        if (!beanPropertySubscriptions.containsKey(annotation.value())) {
                            beanPropertySubscriptions.put(annotation.value(), new HashSet<BeanPropertyHolder>());
                        }
                        beanPropertySubscriptions.get(annotation.value()).add(new BeanPropertyHolder(bean, field));
                    } else {
                        log.info("Leaving field [{}] of class [{}] with default value", new Object[]{field.getName(), bean.getClass()
                                .getCanonicalName()});
                    }
                }
            }
        });
    }

    private void setConfigurationPropertiesBean(final Object bean){

        ReloadableConfigurationProperties annotation = bean.getClass().getAnnotation(ReloadableConfigurationProperties.class);
        if(null!=annotation){
            ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() {

                @Override
                public void doWith(final Field field) throws IllegalArgumentException, IllegalAccessException {

                    final Object property = getProperty(annotation.prefix() + "." + field.getName(), field.getType());
                    if (null != property) {
                        ReflectionUtils.makeAccessible(field);
                        field.set(bean, property);
                    } else {
                        log.info("Leaving field [{}] of class [{}] with default value", new Object[]{field.getName(), bean.getClass()
                                .getCanonicalName()});
                    }
                }
            });

            if (!beanConfigurationPropertiesSubscriptions.containsKey(annotation.value())) {
                beanConfigurationPropertiesSubscriptions.put(annotation.value(), new HashSet<BeanPropertyHolder>());
            }
            beanConfigurationPropertiesSubscriptions.get(annotation.value()).add(new BeanPropertyHolder(bean, null));
        }
    }

    private void validatePropertyAvailableOrDefaultSet(final Object bean, final Field field, final ReloadableProperty annotation, final Object property)
            throws IllegalArgumentException, IllegalAccessException {
        if (null == property && fieldDoesNotHaveDefault(field, bean)) {
            throw new BeanInitializationException(String.format("No property found for field annotated with @ReloadableProperty, "
                    + "and no default specified. Property [%s] of class [%s] requires a property named [%s]", field.getName(), bean.getClass()
                    .getCanonicalName(), annotation.value()));
        }
    }

    private void validateFieldNotFinal(final Object bean, final Field field) {
        if (Modifier.isFinal(field.getModifiers())) {
            throw new BeanInitializationException(String.format("Unable to set field [%s] of class [%s] as is declared final", field.getName(), bean.getClass()
                    .getCanonicalName()));
        }
    }

    private boolean fieldDoesNotHaveDefault(final Field field, final Object value) throws IllegalArgumentException, IllegalAccessException {
        try {
            return (null == field.get(value));
        }
        catch (final NullPointerException e) {
            return true;
        }
    }

    private Object getProperty(String key,Class targetType) {
        if(LetoManage.isEnable()){
            return LetoSpringContext.getReloadProperties().get(key);
        }else{
            return LetoSpringContext.getBaseApplicationContext().getEnvironment().getProperty(key,targetType);
        }
    }
}
